<?php

// Copyright (c) 2010, Meadhbh S. Hamrick
// All rights reserved.
// 
// Redistribution  and use  in source  and  binary forms,  with or  without
// modification, are  permitted provided that the  following conditions are
// met:
// 
// * Redistributions of source code must retain the above copyright notice,
//   this list of conditions and the following disclaimer.
// 
// * Redistributions  in binary  form  must reproduce  the above  copyright
//   notice, this  list of conditions  and the following disclaimer  in the
//   documentation and/or other materials provided with the distribution.
// 
// * Neither  the  name  of  the  Project  Meadhbh nor  the  names  of  its
//   contributors may be  used to endorse or promote  products derived from
//   this software without specific prior written permission.
// 
// THIS SOFTWARE IS PROVIDED BY  THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
// IS" AND  ANY EXPRESS OR  IMPLIED WARRANTIES, INCLUDING, BUT  NOT LIMITED
// TO,  THE  IMPLIED  WARRANTIES  OF  MERCHANTABILITY  AND  FITNESS  FOR  A
// PARTICULAR  PURPOSE ARE  DISCLAIMED.  IN NO  EVENT  SHALL THE  COPYRIGHT
// HOLDER OR  CONTRIBUTORS BE LIABLE FOR ANY  DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
// TO, PROCUREMENT OF  SUBSTITUTE GOODS OR SERVICES; LOSS  OF USE, DATA, OR
// PROFITS; OR BUSINESS  INTERRUPTION) HOWEVER CAUSED AND ON  ANY THEORY OF
// LIABILITY,  WHETHER IN  CONTRACT, STRICT  LIABILITY, OR  TORT (INCLUDING
// NEGLIGENCE  OR OTHERWISE)  ARISING IN  ANY WAY  OUT OF  THE USE  OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

/**
 * A simple implementation of the Dynamic Structured Data standard.
 *
 * Dynamic Structured Data (DSD) is a standard for representing and
 * communicating application data in a platform neutral manner. It is
 * defined in the internet draft draft-hamrick-vwrap-data-00, available
 * at the IETF web site at:
 *
 *   https://datatracker.ietf.org/doc/draft-hamrick-vwrap-data/
 *
 * The DSD draft defines an abstract type system with is reified here
 * using a collection of PHP classes. This implementation assumes it
 * will be run in the context of a web service and that it will be
 * handling requests from client applications using HTTP. The DSD
 * specifications describe three transfer syntaxes based on XML, JSON and
 * a home-grown tagged binary format. This implementation provides functions
 * to convert request bodies in XML, JSON or Binary into PHP Objects so they
 * may be directly queried and manipulated. It also provides functions to
 * convert PHP classes into valid DSD messages.
 *
 * The DSD draft also defines an "interface definition schema" that may be
 * used by web services to see whether a message recieved from a client is
 * what the server was expecting. This implementation also provides this
 * function as well.
 *
 * @author Meadhbh S. Hamrick <OhMeadhbh@gmail.com>
 * @copyright Copyright (C) 2010, Meadhbh S. Hamrick. All Rights Reserved.
 */

abstract class DSD {

  protected $content;

  public static function fromXML( $input ) {
    $parser = new DSDXMLParser( $input );
    $parser->parse();
    return( $parser->getContent() );
  }

  public function getContent ( ) {
    return( $this->content );
  }

  public function at( $index=0 ) {
    if( is_array( $this->content ) ) {
      $rv = $this->content[ $index ];
    } else {
      $rv = NULL;
    }

    if( NULL == $rv ) {
      $rv = new DSDNil();
    }
    
    return( $rv );
  }

  public function setContentFromString( $content ) {
    $this->content = "" . $content;
  }

  public function toXML () {
    $rv  = $this->xmlPrologue();
    $rv .= $this->toXMLWithPadding( "" );
    return( $rv );
  }

  protected function xmlPrologue () {
    return( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" );
  }

  protected function toXMLWithPadding( $padding ) {
    $rv  = $padding;
    $rv .= "<" . $this->xmlTagName() . ">";

    if( $this->isCollection() ) {
      $rv .= $this->toXMLCollectionWithPadding( $padding . "  " );
      $rv .= $padding;
    } else {
      $rv .= str_replace( "&#039;", "&apos;", htmlspecialchars( $this->toString(), ENT_QUOTES ) );
    }

    $rv .= "</" . $this->xmlTagName() . ">\n";

    return( $rv );
  }

  public function isCollection () {
    return( false );
  }

  public function isNil() {
    return( false );
  }

  public function isBoolean() {
    return( false );
  }

  public function isInteger() {
    return( false );
  }

  public function isReal() {
    return( false );
  }

  public function isDate() {
    return( false );
  }

  public function isString() {
    return( false );
  }

  public function isUUID() {
    return( false );
  }

  public function isURI() {
    return( false );
  }

  public function isBinary() {
    return( false );
  }

  public function isArray() {
    return( false );
  }

  public function isMap() {
    return( false );
  }

  abstract protected function toString ();

  abstract protected function xmlTagName ();
}

class DSDNil extends DSD {
  public function __construct( $content=NULL ) {
    $this->content = NULL;
  }

  public function setContentFromString( $content ) {
  }

  protected function toXMLWithPadding( $padding ) {
    $rv  = $padding . "<nil/>\n";
    return( $rv );
  }

  protected function toString () {
    return( "NULL" );
  }

  protected function xmlTagName () {
    return( "nil" );
  }
}

class DSDBoolean extends DSD {
  public function __construct( $content=FALSE ) {
    $this->content = $content;
  }

  public function setContentFromString( $content ) {
    if( "true" == $content ) {
      $this->content = true;
    } else {
      $this->content = false;
    }
  }

  protected function toString () {
    return( $this->content ? "true" : "false" );
  }

  protected function xmlTagName () {
    return( "boolean" );
  }
}

class DSDInteger extends DSD {
  public function __construct( $content=0) {
    $this->content = $content;
  }

  public function setContentFromString( $content ) {
    $this->content = (int) $content;
  }

  protected function toString () {
    return( "" + (string) $this->content );
  }

  protected function xmlTagName () {
    return( "integer" );
  }
}

class DSDReal extends DSD {
  public function __construct( $content=0.0 ) {
    $this->content = $content;
  }

  public function setContentFromString( $content ) {
    $this->content = (double) $content;
  }

  protected function toString () {
    return( "" + (string) $this->content );
  }

  protected function xmlTagName () {
    return( "real" );
  }
}

class DSDDate extends DSD {
  public function __construct( $content="1970-01-01T00:00:00Z" ) {
    $this->content = $content;
  }

  protected function toString () {
    return( $this->content );
  }

  protected function xmlTagName () {
    return( "date" );
  }
}

class DSDString extends DSD {
  public function __construct( $content="" ) {
    $this->content = $content;
  }

  protected function toString () {
    return( $this->content );
  }

  protected function xmlTagName () {
    return( "string" );
  }
}

class DSDUUID extends DSD {
  public function __construct( $content="00000000-0000-0000-0000-000000000000" ) {
    $this->content = $content;
  }

  protected function toString () {
    return( $this->content );
  }

  protected function xmlTagName () {
    return( "uuid" );
  }
}

class DSDURI extends DSD {
  public function __construct( $content="" ) {
    $this->content = $content;
  }

  protected function toString () {
    return( $this->content );
  }

  protected function xmlTagName () {
    return( "uri" );
  }
}

class DSDBinary extends DSD {
  public function __construct( $content="" ) {
    $this->content = $content;
  }

  protected function toString () {
    return( $this->content );
  }

  protected function xmlTagName () {
    return( "binary" );
  }
}

class DSDArray extends DSD {
  public function __construct( $content=NULL ) {
    if( is_array( $content ) ) {
      $this->content = $content;
    } else {
      $this->content = array();
    }
  }

  protected function toString () {
    //todo: do something better here
    return( "" );
  }

  protected function xmlTagName () {
    return( "array" );
  }

  public function isCollection () {
    return( true );
  }

  protected function toXMLCollectionWithPadding( $padding ) {
    $rv = "\n";
    foreach ( $this->content as $value ) {
      $rv .= $value->toXMLWithPadding( $padding );
    }
    return( $rv );
  }

  public function setArray( $array ) {
    $this->content = $array;
  }
}

class DSDMap extends DSD {
  public function __construct( $content=NULL ) {
    if( is_array( $content ) ) {
      $this->content = $content;
    } else {
      $this->content = array();
    }
  }

  protected function toString () {
    //todo: do something better here
    return( "" );
  }

  protected function xmlTagName () {
    return( "map" );
  }

  public function isCollection () {
    return( true );
  }

  protected function toXMLCollectionWithPadding( $padding ) {
    $rv = "\n";
    foreach ( $this->content as $key => $value ) {
      $rv .= $padding . "<key>" . $key . "</key>\n";
      $rv .= $value->toXMLWithPadding( $padding );
    }
    return( $rv );
  }

  public function setMap( $map ) {
    $this->content = $map;
  }

}

class DSDXMLParser {
  private $input;
  private $in_scalar_tag;
  private $in_key_tag;
  private $data;
  private $stack;
  private $index;
  private $last;
  private $keys;
  private $last_key;

  public function __construct( $input ) {
    $this->input = $input;
    $this->in_scalar_tag = false;
    $this->stack = array();
    $this->index = -1;
    $this->in_key_tag = false;
    $this->last_key = "";
    $this->keys = array();
  }

  public function parse () {
    $parser = xml_parser_create();

    xml_set_object( $parser, $this );
    xml_set_element_handler( $parser, "open", "close" );
    xml_set_character_data_handler( $parser, "data" );
    xml_parse( $parser, $this->input );

    xml_parser_free( $parser );
  }

  private function open( $parser, $tag, $attributes ) {
    $enter_collection = false;

    switch( $tag ) {
    case "NIL":
      $current = new DSDNil();
      break;

    case "BOOLEAN":
      $current = new DSDBoolean();
      break;

    case "INTEGER":
      $current = new DSDInteger();
      break;

    case "REAL":
      $current = new DSDReal();
      break;

    case "DATE":
      $current = new DSDDate();
      break;

    case "STRING":
      $current = new DSDString();
      break;

    case "UUID":
      $current = new DSDUUID();
      break;

    case "URI":
      $current = new DSDURI();
      break;

    case "BINARY":
      $current = new DSDBinary();
      break;

    case "ARRAY":
      $current = new DSDArray();
      $enter_collection = true;
      break;

    case "MAP":
      $current = new DSDMap();
      $enter_collection = true;
      break;

    case "KEY":
      $this->in_key_tag = true;
      break;
    }

    if( ! $this->in_key_tag ) {
      array_push( $this->stack, $current );
      $this->index++;
    }

    if( ! $enter_collection ) {
      $this->in_scalar_tag = true;
    }
  }

  private function close( $parser, $tag ) {
    if( $this->in_key_tag ) {
      $this->last_key = $this->data;
      $this->in_key_tag = false;
      array_push ( $this->keys, $this->last_key );
    } else {

      $current=array_pop( $this->stack );
      $this->index--;

      if( ( $tag != "MAP" ) && ( $tag != "ARRAY" ) ) {
	$current->setContentFromString( $this->data );
      }

      if( $this->index < 0 ) {
	$this->last = $current;
      } else {
	$collection = $this->stack[ $this->index ];

	$inner_collection = $collection->getContent();

	switch( get_class( $collection ) ) {
	case "DSDArray":
	  array_push( $inner_collection, $current );
	  $collection->setArray( $inner_collection );
	  break;
	case "DSDMap":
	  $this->last_key = array_pop( $this->keys );
	  $inner_collection[ $this->last_key ] = $current;
	  $collection->setMap( $inner_collection );
	  break;
	}
      }
    }

    $this->in_scalar_tag = false;
    $this->data = "";
  }

  private function data( $parser, $data ) {
    if( $this->in_scalar_tag ) {
      $this->data .= $data;
    }
  }

  public function getContent() {
    $rv = $this->last;
    if( NULL == $rv ) {
      $rv = new DSDNil();
    }
    return( $rv );
  }
}

?>
